﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.XPath;
using SWD = System.Web.Services.Description;

namespace MockingBird.Wsdl.Parser
{

    /// <summary>
    /// Describes the structure of a given WSDL in a very intuitive, flat way.
    /// It requires a WsdlRetriever which handles the grunt work of getting a wsdl which of course
    /// may be in multiple parts
    /// </summary>
    public class WsdlDescriptor : IWsdlDescriptor
    {
        private IWsdlRetriever wsdlRetriever;
        private XDocument wsdlDocument;
        private SWD.ServiceDescription svcDescription; //uses this object to get some stuff out of the wsdl
        private XmlSchemaSet schemaSet = new XmlSchemaSet();

        #region "public properties"
        /// <summary>
        /// Gets the service name
        /// </summary>
        // use automatic property. Setting can be done, but only inside this class
        public string ServiceName { get; private set; }

        /// <summary>
        /// Gets all the available endpoints. 
        /// </summary>
        // use automatic property. Setting can be done, but only inside this class
        public List<Endpoint> Endpoints { get; private set; }

        public SWD.BindingCollection Bindings { get; private set; }

        public List<string> InterfaceNames { get; private set; }

        public List<Operation> Operations { get; private set; }

        public List<string> OperationNames { get; private set; }

        public XmlSchemas Schemas
        {
            get
            {
                return svcDescription.Types.Schemas;
            }

        }

        public XmlSchemaSet SchemaSet
        {

            get
            {
                foreach (XmlSchema schema in Schemas)
                {
                    schemaSet.Add(schema);

                }
                return schemaSet;
            }
        }
        #endregion

        public WsdlDescriptor(IWsdlRetriever retriever)
        {
            if (retriever == null)
                throw new ArgumentNullException("retriever", "retriever is null.");

            //assign it to the member variable
            wsdlRetriever = retriever;

            GetWsdlDocumentFromRetriever();

            ValidateWsdl();

            ProcessWsdlDocument();
        }
        private void GetWsdlDocumentFromRetriever()
        {
            wsdlDocument = wsdlRetriever.GetWsdl();
        }
        private void ValidateWsdl()
        {
            if (wsdlDocument == null)
                throw new InvalidDataException("WSDL was null.Cannot proceed with operations");
        }

        #region "private methods to parse and process the wsdl"
        /// <summary>
        /// The heart of the processing
        /// </summary>
        private void ProcessWsdlDocument()
        {
            //Setup the service description
            SetupSvcDescription();
            //now continue processing
            SetServiceInfo();
            SetEndpointInfo();
            SetBindingInfo();
            SetInterfaceInfo();
            SetOperationInfo();
        }
        private void SetupSvcDescription()
        {
            //We have an XDocument but that is not directly usable by the ServiceDescription
            //so we go via a memory stream
            MemoryStream memStream = new MemoryStream();
            XmlWriter writer = XmlWriter.Create(memStream);
            wsdlDocument.WriteTo(writer);
            writer.Flush();
            //Now rewind the stream, so we can read from it
            memStream.Position = 0;
            svcDescription = SWD.ServiceDescription.Read(memStream);
            //job over- cleanup the memory stream
            memStream.Close();
            memStream.Dispose();
        }
        private void SetServiceInfo()
        {
            //Get the service name.We only care about the single service now
            ServiceName = svcDescription.Services[0].Name;


        }

        private void SetEndpointInfo()
        {
            //Sets up information about the collection of endpoints
            //TODO: Could be refactored into an Endpoints Collection class behavior
            List<XElement> endpointElements = wsdlDocument.XPathSelectElements(@"//*[local-name()='port']").ToList();
            Endpoints = new List<Endpoint>(endpointElements.Count);
            foreach (XElement element in endpointElements)
            {
                Endpoint ep = new Endpoint(element);
                ep.Fill();
                Endpoints.Add(ep);
            }
        }

        private void SetBindingInfo()
        {
            //currently this is enough. we can get more stuff out of this later
            Bindings = svcDescription.Bindings;

        }

        private void SetInterfaceInfo()
        {
            InterfaceNames = new List<string>(svcDescription.PortTypes.Count);
            foreach (SWD.PortType interfaceType in svcDescription.PortTypes)
            {
                InterfaceNames.Add(interfaceType.Name);
            }

        }

        private void SetOperationInfo()
        {
            Operations = new List<Operation>();
            OperationNames = new List<string>();

            //setup all the operations
            foreach (SWD.PortType pType in svcDescription.PortTypes)
            {
                foreach (SWD.Operation sourceOp in pType.Operations)
                {
                    //Setup an operation from the Service description operation property
                    Operation op = new Operation(svcDescription, sourceOp);
                    //Now add it to our collection
                    Operations.Add(op);
                    //extract the name and add it to the list of names
                    OperationNames.Add(op.Name);
                }

            }

        }

        #endregion

        #region "public methods"

        /// <summary>
        /// Gets an operation from the collection of operations
        /// </summary>
        /// <param name="operationName">the name of the operation we are looking for</param>
        /// <returns>Operation</returns>
        public Operation FindOperation(string operationName)
        {
            Operation locatedOperation = null;
            //We dont want to expose too much of List<> behavior so its better to have an explicit
            //method.
            foreach (Operation op in this.Operations)
            {
                if (op.Name.Equals(operationName))
                {
                    locatedOperation = op;
                    break;
                }


            }
            //TODO: if we didnt find it we should probably throw a custom exception
            return locatedOperation;

        }

        public OperationMessage FindMessage(string messageName)
        {
            OperationMessage locatedMessage = null;

            foreach (Operation op in this.Operations)
            {
                locatedMessage = op.FindMessage(messageName);
                if (locatedMessage != null) break;
            }

            return locatedMessage;
        }

        #endregion
    }
}
